ScalaCache + Caffeine + Cats Effectでインメモリキャッシュを実装する
はじめに
Scalaでインメモリキャッシュを実装する必要があってのでScalaCacheというライブラリを試してみました。
ScalaCacheとは
ScalaCacheはいくつかのキャッシュライブラリのファサードとなるライブラリです。 次のような特徴があります。
- Scalaでの利用に適したシンプルなAPIを提供している
- 少ない手間でScalaのアプリケーションにキャッシュを導入できる
A facade for the most popular cache implementations, with a simple, idiomatic Scala API. Use ScalaCache to add caching to any Scala app with the minimum of fuss.
キャッシュの実装としては以下を利用できます。
- Google Guava
- Memcached
- Ehcache
- Redis
- Caffeine
- cache2k
- OHC
使い方
Getting Startedにもありますが下記のように使います。
import scalacache._ import scalacache.caffeine.CaffeineCache import cats.implicits._ final case class Cat(id: Int, name: String, colour: String) object Cat { val duchess:Cat = Cat(1, "Duchess", "white") val thomas:Cat = Cat(2, "Thomas", "orange") val marie:Cat = Cat(3, "Marie", "white") } //キャッシュインスタンス生成: 実装はCaffeine implicit val cache:Cache[Cat] = CaffeineCache[Cat] //モードの指定(Try) import scalacache.modes.try_._ //エントリを追加 put(Cat.duchess.id)(Cat.duchess) //参照 get(Cat.duchess.id) //Success(Some(Cat(1,Duchess,white))) //キャッシュミス get(Cat.thomas.id) //Success(None) //削除 remove(Cat.duchess.id) //Success(()) remove(Cat.marie.id) //Success(()) //キャッシュの更新処理を指定 cachingF(Cat.thomas.id) (ttl = None)(Try { println(s"where is the cat(id=${Cat.thomas.id}) ??") Cat.thomas }) //where is the cat(id=2) ?? //Success(Cat(2,Thomas,orange))
以下ポイントをいくつか説明します。
キャッシュインスタンスの生成と、モードの指定
使用するキャッシュの実装と、コンテナの型に合わせたmodeをimplicit
で定義しておきます。
ここでは実装としてJavaのインメモリキャッシュの実装であるCaffeine
を選択し、コンテナ型はTry
を指定するのでscalacache.modes.try_._
をインポートしています。
//キャッシュインスタンス生成: 実装はCaffeine implicit val cache:Cache[Cat] = CaffeineCache[Cat] //モードの指定(Try) import scalacache.modes.try_._
キャッシュの実装の詳細
キャッシュの実装にどのようなものがあるかや初期化時の詳細についてはCache Implementationsに詳しいです。 (Caffeineでは詳細の指定はスキップできますが、例えばRedisを使う実装ではRedisサーバーのホスト名とポートを指定する必要があります。)
mode(コンテナ型の指定)
ScalaCacheではキャッシュへの操作を任意の副作用コンテナでラップできます。
例えばTry
, Future
, cats.effect.IO
などです。
これによりキャッシュレイヤのAPIの型をプロジェクトで使っている型に柔軟に合わせることができます。
コンテナ型はキャッシュ操作関数のimplicit parameter mode
で指定します。
キャッシュの操作
キャッシュの操作はscalacache
パッケージに定義された関数で行います。
これらの関数には前述のキャッシュインスタンスとモードをimplicit parameter
として指定します。
//エントリを追加 put(Cat.duchess.id)(Cat.duchess) //参照 get(Cat.duchess.id) //Success(Some(Cat(1,Duchess,white))) //キャッシュミス get(Cat.thomas.id) //Success(None)
cachingF
cachingF
によってキャッシュの更新処理を指定することができます。
更新処理はモードで指定したコンテナ型のものを指定します。
これによって既存のコードでキャッシュなしで値の参照をしていたコードを、cachingF
に置き換えるだけでキャッシュ機能を追加することができます。
//猫ちゃんを探す骨の折れる仕事 def getCat(id:Int): Try[Cat] = ??? //before(キャッシュなし) getCat(Cat.thomas.id) //after(キャッシュあり) cachingF(Cat.thomas.id) (ttl = None)(getCat(Cat.thomas.id))
Cats Effectと組み合わせて使う例
さてここで、Cats Effectを使って実装されているデータベースや外部APIの呼び出し処理を再利用する形でキャッシュを使ったサンプルを作ってみます。
この例ではcachingF
の第2引数のttl
を指定して、キャッシュが一定時間経過後に無効になることを確認しています。
先ほどまでのTry
の例とキャッシュの使い方はほとんど変わっていないことがわかると思います。
package example import cats.effect.{ExitCode, IO, IOApp} import cats.implicits._ import scalacache._ import scalacache.caffeine._ import scala.concurrent.duration._ object IOExample extends IOApp { // キャッシュのエントリの型 type E = String // キャッシュの実装 implicit val cache: Cache[E] = CaffeineCache[E] // IO用のモード implicit val mode: Mode[IO] = scalacache.CatsEffect.modes.async // キャッシュのTTL val ttl = 100.millis // データソース(API, DB)から値を参照する既存のコンポーネント def fetch(key: String): IO[E] = IO(println(s"cache missed for ${key}")) *> IO.pure(s"value-${key}") // キャッシュから値を参照する、キャッシュに存在しなければデータソースを参照 def find(key:String): IO[E] = cachingF(key)(ttl.some)(fetch(key)) override def run(args: List[String]): IO[ExitCode] = for { _ <- find("k1") _ <- find("k2") _ <- find("k1") _ <- find("k2") _ <- timer.sleep(ttl) *> IO(println("sleep")) //キャッシュが無効になるまで待つ _ <- find("k1") _ <- find("k2") _ <- find("k1") _ <- find("k2") } yield ExitCode.Success }
出力
cache missed for k1 cache missed for k2 sleep cache missed for k1 cache missed for k2
まとめ
ScalaのキャッシュライブラリScalaCacheを試してみました。 ScalaCacheを使うと既存のコードを大きく変更せずにキャッシュ機能を追加することができます。また必要に応じてキャッシュの実装も切り替えできます。
おまけ
scalacache-cats-effect
には依存ライブラリとしてcats-effectが含まれています。プロジェクトで使っているバージョンと一致しない場合には下記のように除外した方がいいです。
libraryDependencies += "com.github.cb372" %% "scalacache-cats-effect" % scalaCacheVersion excludeAll(ExclusionRule(organization="org.typelevel"))